<!--
 * @Author: TonyJiangWJ
 * @Date: 2024-12-09 16:08:31
 * @Last Modified by: TonyJiangWJ
 * @Last Modified time: 2024-12-16 18:22:40
 * @Description: github: https://github.com/TonyJiangWJ/WebSocketWidgetInspector
-->
<!DOCTYPE html>
<html>

<head>
  <meta charset="UTF-8">
  <title>控件可视化</title>
  <link href="./css/materialdesignicons.min.css" rel="stylesheet">
  <link href="./css/vuetify.min.css" rel="stylesheet">
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, minimal-ui">
  <style>
    .v-treeview-node__label {
      overflow: unset;
    }

    .v-treeview {
      overflow: scroll;
      min-height: 788px;
    }

    /* 避免宽度限制 导致层级过深导致UI层级不明显 */
    .v-treeview-node__children {
      width: 1200px;
    }

    .float_box_discover {
      background-color: #00000000;
      border-radius: 5px;
      /* width: 150px;			 */
      text-align: center;
      position: absolute;
    }

    .v-sheet.v-list {
      background: #00000000;
    }

    .code {
      background-color: #e4e4e4;
      border-radius: 5px;
      padding: 0 5px;
      margin: 0 5px;
    }

    @media screen and (min-device-width: 500px) {
      .left-tree {
        max-width: 100%;
        flex: 0 0 100%;
      }

      .right-canvas {
        max-width: 100%;
        flex: 0 0 100%;
      }
    }

    @media screen and (min-device-width: 1030px) {
      .left-tree {
        max-width: 60%;
        flex: 0 0 60%;
      }

      .right-canvas {
        max-width: 40%;
        flex: 0 0 40%;
      }
    }
  </style>
</head>

<style>
  td {
    word-wrap: break-word;
    word-break: break-word;
    white-space: normal;
  }

  [v-cloak] {
    display: none;
  }
</style>

<body>
  <div id="app" v-cloak>
    <v-app>
      <v-main>
        <v-container>
          <div v-show="findTargetObj" class="float_box_discover"
            :style="{ 'left': winPos.x +'px', 'top': winPos.y+'px' }">
            <v-list>
              <v-list-item>
                <v-btn elevation="2" @click="showDetail(null)">查看详情</v-btn>
              </v-list-item>
              <v-list-item>
                <v-btn elevation="2" @click="removeItem(null)">排除</v-btn>
              </v-list-item>
              <v-list-item>
                <v-btn elevation="2" @click="openTarget(null)">在树中显示</v-btn>
              </v-list-item>
              <v-list-item v-if="connected">
                <v-btn elevation="2" @click="clickCenter(null)">点击中心点</v-btn>
              </v-list-item>
              <v-list-item v-if="connected">
                <v-btn elevation="2" @click="clickPos(null)">点击当前坐标</v-btn>
              </v-list-item>
            </v-list>
          </div>
          <v-row dense no-gutters>
            <v-col><span>连接状态：</span><span :style="{color:connected?'green':'orange'}">{{connectStatus}}</span></v-col>
            <v-col>
              <v-text-field label="服务地址" v-model="wsServerUrl"></v-text-field>
            </v-col>
            <v-col>
              <v-btn v-if="!connected" elevation="2" @click="connectServer()" :loading="connecting">连接服务</v-btn>
              <v-btn v-if="connected" elevation="2" @click="requestWidgetInfo()" :loading="requesting">获取当前控件信息</v-btn>
              <v-btn elevation="2" @click="loadHisInfo()">加载历史数据</v-btn>
            </v-col>
          </v-row>
          <v-row dense no-gutters>
            <v-col style="margin-top: -20px; margin-left: 5px;">
              设备宽高：{{deviceWidth}} {{deviceHeight}} 总控件数：{{totalWidgets}} 筛选的：{{selectedItems?selectedItems.length:0}}
            </v-col>
          </v-row>
          <v-row justify="start" no-gutters dense>
            <v-col>
              <v-checkbox v-model="visibleOnly" label="只显示可见节点" dense></v-checkbox>
            </v-col>
            <v-col>
              <v-checkbox v-model="hasContent" label="只显示有内容节点[id,desc,text]" dense></v-checkbox>
            </v-col>
            <v-col>
              <v-checkbox v-model="clickableOnly" label="只显示clickable节点" dense></v-checkbox>
            </v-col>
            <v-col>
              <v-checkbox v-model="inScreen" label="只显示屏幕内节点" dense></v-checkbox>
            </v-col>
            <v-col dense>
              <v-checkbox v-model="advance" label="高级功能" dense></v-checkbox>
            </v-col>
          </v-row>
          <template v-if="advance">
            <v-row>
              <v-col>
                <p>过滤函数是在树结构中筛选需要的节点，选择函数是自动选中并绘制筛选出来的节点，推荐写法为AutoJS UiSelector的形式。</p>
                <p>UiSelector写法：<span class="code">boundsInside(100, 500, 700,
                    2000).className('android.widget.TextView').filter(v
                    => v.boundsInfo.right - v.boundsInfo.left <= 200)</span>筛选区域内的宽度小于200的TextView节点</p>
                <p>匿名函数写法：<span class="code">node =>
                    /.*签到.*/.test(node.desc)</span>筛选节点desc匹配的节点，匿名函数更自由可以根据UiObject的属性字段自定义筛选</p>
              </v-col>
            </v-row>
            <v-row>
              <v-col cols="6">
                <v-textarea v-model="filterFuncStr" label="过滤函数" :rows="3"
                  placeholder="如使用 UiSelector 写法为: boundsInside(500, 2000, 700, 2300)" @blur="handleFuncStrChange">
                </v-textarea>
              </v-col>
              <v-col cols="6">
                <v-textarea v-model="selectorFuncStr" label="选择函数" :rows="3"
                  placeholder="如使用匿名函数，写法为：node => /.*签到.*/.test(node.desc)" @blur="handleSelectorFuncStrChange">
                </v-textarea>
              </v-col>
            </v-row>
          </template>
          <v-row v-if="connected" style="margin-top: -20px;">
            <v-col dense>
              <v-checkbox v-model="showExecuteCode" label="执行脚本" dense></v-checkbox>
            </v-col>
            <v-col dense>
              <v-btn v-if="showExecuteCode" elevation="2" @click="sendCode()" :loading="requesting">发送执行</v-btn>
            </v-col>
          </v-row>
          <template v-if="showExecuteCode">
            <v-row>
              <v-col>
                <v-textarea v-model="executeCode" label="执行脚本" :rows="3" placeholder="输入需要执行的脚本">
                </v-textarea>
              </v-col>
            </v-row>
            <v-row>
              <v-col>
                <v-textarea v-model="executePath" label="执行路径"
                  placeholder="输入脚本执行时所在的路径，以便可以通过require加载js 例如：/sdcard/脚本/MyScripts/" :rows="1"></v-textarea>
              </v-col>
            </v-row>
          </template>
          <v-row>
            <v-col>
              <v-btn elevation="2" @click="openAll">全部打开</v-btn>
              <v-btn elevation="2" @click="closeAll">全部关闭</v-btn>
              <v-btn elevation="2" @click="selectAll">全部选中</v-btn>
            </v-col>
          </v-row>
          <v-row>
            <v-col class="left-tree">
              <v-switch v-model="selectionIndependent" flat
                :label="`选择模式: ${selectionIndependent?'独立节点':'叶子节点'}`"></v-switch>
              <v-treeview ref="treeNode" hoverable selectable dense :items="items" :open="opened" :active="treeActive"
                :selection-type="selectionType" v-model="treeSelectedItems" :key="selectionType"
                @update:open="handleOpen" @update:active="handleActive" :style="{height: treeHeight}">
                <template v-slot:label="{ item }">
                  <span :style="{color: item.root.visible?'black':'gray'}"
                    @click="showDetail(item)">{{item.name}}</span>
                </template>
              </v-treeview>
            </v-col>
            <v-col class="right-canvas">
              <v-checkbox v-model="draw3DInfo" label="显示3D图"></v-checkbox>
              <div v-show="!draw3DInfo">
                <div style="margin-top: -20px; margin-left: 5px;">
                  <span>x: {{devicePos.x}} y: {{devicePos.y}} <span
                      :style="{color: posBkColor, backgroundColor: posColor}">{{posColor}}</span></span>
                </div>
                <canvas id="myCanvas" width="400" height="889" style="border:1px solid #000000;">
                  您的浏览器不支持 HTML5 canvas 标签。
                </canvas>
              </div>
              <div v-show="draw3DInfo">
                <div style="margin-top: -20px; margin-left: 5px;">
                  <span>左键旋转，右键平移，中键缩放</span>
                </div>
                <div id="container" style="width: 600px;height: 900px;background-color: gray;"></div>
              </div>
            </v-col>
          </v-row>

          <v-row v-show="connected" style="margin-top: -20px;">
            <v-col dense>
              <v-checkbox v-model="showObjectJson" label="显示控件JSON详情" dense></v-checkbox>
            </v-col>
          </v-row>
          <template v-if="showObjectJson">
            <v-row>
              <v-col>
                <v-textarea v-model="uiobjects" label="uiobjects.json内容" :rows="5" row-height="15">
                </v-textarea>
              </v-col>
            </v-row>
            <v-row>
              <v-col>
                <v-textarea v-model="imgBase64" label="图片base64" :rows="5" row-height="15">
                </v-textarea>
              </v-col>
            </v-row>
          </template>
        </v-container>
        <v-dialog v-model="dialog" width="700">
          <v-card-title class="text-h5 grey lighten-2">
            控件详情
          </v-card-title>
          <v-card>
            <div style="padding: 2rem;max-height: 600px;overflow-y: scroll;">
              <div v-for="(item, i) in uiObjectItems" :key="i">
                <v-row>
                  <v-col cols="4">{{item.label}}:</v-col>
                  <v-col cols="8">{{item.value}}</v-col>
                </v-row>
              </div>
            </div>
            <v-card-actions v-if="stackItems.length > 1" style="background-color: white;">
              <v-spacer>总堆叠数：{{showDetailIdx+1}}/{{stackItems.length}}</v-spacer>
              <v-btn color="primary" text :disabled="showDetailIdx==0" @click="showDetailIdx--">
                上一个
              </v-btn>
              <v-btn color="primary" text @click="showDetailIdx++" :disabled="showDetailIdx >= stackItems.length - 1">
                下一个
              </v-btn>
            </v-card-actions>
            <v-card-actions style="background-color: white;justify-content: right;">
              <div style="padding-bottom: 16px;padding-right: 5px;">
                <v-btn @click="() => {openTarget(itemId);dialog=false}">在树中显示</v-btn>
                <v-btn @click="() => {clickCenter();dialog=false}">点击中心点</v-btn>
                <v-btn @click="removeItem()">排除</v-btn>
                <v-btn @click="()=>dialog=false" color="primary">关闭</v-btn>
              </div>
            </v-card-actions>
          </v-card>

        </v-dialog>

        <v-dialog v-model="hisDialog" width="700">
          <v-card>
            <v-card-title class="text-h5 grey lighten-2">
              历史控件记录，目前最多保存{{maxHisData}}条
            </v-card-title>

            <v-list style="max-height: 500px;overflow-y: scroll;">
              <v-list-item v-for="hisObject in hisObjects" :key="hisObject.timestamp"
                style="border-bottom: solid 1px rgba(0,0,0,0.12)" @click="renderHisObject(hisObject)">
                <v-list-item-content style="display: block;">
                  <div style="display: inline; float: left; padding: 5px;padding-right: 20px;">
                    <img :src="hisObject.smallImg" style="max-height: 40px;" />
                  </div>
                  <div style="display: inline; float: left;">
                    <v-list-item-title>{{hisObject.timestamp|stamp2Str}}</v-list-item-title>
                    <v-list-item-subtitle>
                      <div>
                        <span>
                          总控件数：{{hisObject.totalWidgets}}
                        </span>
                        <span>
                          设备宽高：{{hisObject.deviceWidth}} {{hisObject.deviceHeight}}
                        </span>
                      </div>
                      <div>
                        <span>包名：{{hisObject.packageName}}</span>
                      </div>
                    </v-list-item-subtitle>
                  </div>
                  <div style="display: inline;float: right;">
                    <v-btn color="primary" text @click.stop="() => {deleteData(hisObject.timestamp)}">
                      删除
                    </v-btn>
                  </div>
                </v-list-item-content>
              </v-list-item>
            </v-list>

            <v-divider></v-divider>

            <v-card-actions>
              <v-spacer></v-spacer>
              <v-btn color="primary" text @click="hisDialog = false">
                关闭
              </v-btn>
            </v-card-actions>
          </v-card>

        </v-dialog>
    </v-app>
  </div>

  <script src="./js/vue.js"></script>
  <script src="./js/vuetify.js"></script>
  <script type="importmap">
    {
      "imports": {
        "three": "https://cdn.jsdelivr.net/npm/three@0.171.0/build/three.module.js",
        "three/addons/": "https://cdn.jsdelivr.net/npm/three@0.171.0/examples/jsm/"
      }
    }
  </script>
  <!-- 跨域无法加载
  <script type="importmap">
    {
      "imports": {
        "three": "./js/three@0.171.0/build/three.module.js",
        "three/addons/": "./js/three@0.171.0/examples/jsm/"
      }
    }
  </script> -->
  <script src="./js/uiobject-helper.js"></script>
  <script src="./js/ws-helper.js"></script>
  <script src="./js/indexedDbhelper.js"></script>

  <script type="module">

    const render_width = 600
    const render_height = 1200
    import * as THREE from 'three';
    import { OrbitControls } from 'three/addons/controls/OrbitControls.js';
    // Initialize scene, camera, and renderer
    const scene = new THREE.Scene();
    const camera = new THREE.PerspectiveCamera(50, render_width / render_height, 0.1, 2000);
    const renderer = new THREE.WebGLRenderer();
    renderer.setSize(render_width, render_height);

    document.getElementById('container').appendChild(renderer.domElement);
    camera.position.set(0, 0, 350);
    camera.lookAt(0, 0, 0)
    // Add OrbitControls for interaction
    const controls = new OrbitControls(camera, renderer.domElement);
    // 启用平移功能
    controls.enablePan = true;

    // 设置平移速度（可选）
    controls.panSpeed = 1.0;

    // 默认情况下，按住鼠标右键可以平移
    // 可以通过设置 controls.mouseButtons 来自定义操作按钮
    controls.mouseButtons = {
      LEFT: THREE.MOUSE.ROTATE,   // 左键旋转
      MIDDLE: THREE.MOUSE.DOLLY,  // 中键缩放
      RIGHT: THREE.MOUSE.PAN      // 右键平移
    };
    controls.update();
    // // 创建一个共享的 Canvas
    const canvas = document.createElement("canvas");
    const ctx = canvas.getContext("2d");
    // 函数：为矩形生成带文本的纹理 TODO 文字显示效果太差
    function createTextTexture (text, width, height) {
      // 创建一个 Canvas
      // const canvas = document.createElement("canvas");
      canvas.width = width * 10;
      canvas.height = height * 10;
      // console.log('设置canvas 宽高', canvas.width, canvas.height)
      // const ctx = canvas.getContext("2d");
      // 清空 Canvas
      ctx.clearRect(0, 0, canvas.width, canvas.height);

      // 绘制背景
      ctx.fillStyle = "white";
      ctx.fillRect(0, 0, canvas.width, canvas.height);

      // 绘制文本
      ctx.fillStyle = "black";
      ctx.font = "30px SimSun";
      ctx.textAlign = "center";
      ctx.textBaseline = "middle";

      // console.log('create rect with text:', text)
      ctx.fillText(text, canvas.width / 2, canvas.height / 2);
      // 将canvas转图片 然后创建文理
      const dataURL = canvas.toDataURL('image/png'); // 转换为 PNG 图片
      // console.log(dataURL)
      const texture = new THREE.TextureLoader().load(dataURL);
      // 创建纹理
      // const texture = new THREE.CanvasTexture(canvas);
      return texture
    }
    // Function to create a rectangle
    function createRectangle (width, height, depth, color, opacity, text) {
      const geometry = new THREE.BoxGeometry(width, height, depth);
      let material = new THREE.MeshBasicMaterial({
        color,
        transparent: true,   // 启用透明度
        opacity: opacity || 0.8         // 设置透明度（0为完全透明，1为完全不透明）
      });
      if (text) {
        const texture = createTextTexture(text, width, height)

        // 设置纹理的重复和模式
        texture.wrapS = THREE.ClampToEdgeWrapping; // 水平方向
        texture.wrapT = THREE.ClampToEdgeWrapping; // 垂直方向
        texture.minFilter = THREE.LinearFilter; // 缩小纹理时的过滤
        texture.magFilter = THREE.LinearFilter; // 放大纹理时的过滤
        material = new THREE.MeshBasicMaterial({
          color,
          map: texture
        });
      }

      return new THREE.Mesh(geometry, material);
    }

    function createRectOutline (width, height, depth, color, opacity) {
      // 矩形边框
      const rectGeometry = new THREE.PlaneGeometry(width, height, depth); // 创建一个矩形
      const edges = new THREE.EdgesGeometry(rectGeometry); // 提取矩形边框
      const material = new THREE.LineBasicMaterial({ color, opacity: opacity || 0.8 }); // 线条材质
      const rectOutline = new THREE.LineSegments(edges, material); // 创建线段
      return rectOutline
    }

    function createRectangleWithImage (width, height, depth, base64) {

      const geometry = new THREE.BoxGeometry(width, height, depth);
      // 创建 Base64 图片纹理
      const base64Image = base64; // 替换为你的 Base64 图片数据
      const texture = new THREE.TextureLoader().load(base64Image);

      // 设置纹理的重复和模式
      texture.wrapS = THREE.ClampToEdgeWrapping; // 水平方向
      texture.wrapT = THREE.ClampToEdgeWrapping; // 垂直方向
      texture.minFilter = THREE.LinearFilter; // 缩小纹理时的过滤
      texture.magFilter = THREE.LinearFilter; // 放大纹理时的过滤

      // 创建材质并应用纹理
      const material = new THREE.MeshBasicMaterial({ map: texture });

      // 创建矩形网格
      return new THREE.Mesh(geometry, material);
    }

    function clearGroup (group) {
      while (group.children.length > 0) {
        const child = group.children[0]; // 获取第一个子对象

        // 释放几何体资源
        if (child.geometry) {
          child.geometry.dispose();
        }

        // 释放材质资源
        if (child.material) {
          if (Array.isArray(child.material)) {
            // 如果材质是数组，逐一释放
            child.material.forEach((mat) => mat.dispose());
          } else {
            child.material.dispose();
          }
        }

        // 从 Group 中移除子对象
        group.remove(child);
      }
    }

    // Create rectangles and add them to a group
    const group = new THREE.Group();

    function rerender (objects, imageBase64) {
      // console.log('绘制对象列表：', objects)
      // 设置偏移 使左上角顶点位于 0,0,0
      group.position.set(-deviceWidth / 10 / 2, deviceHeight / 10 / 2, 0);
      clearGroup(group)

      const rect1 = createRectangleWithImage(deviceWidth / 10, deviceHeight / 10, 1, imageBase64); // Blue background rectangle

      const rect0 = createRectangle(1, 1, 1, 0xff0000, 1); // Red rectangle
      rect0.position.set(0, 0, -1);
      group.add(rect0)
      rect1.position.set(deviceWidth / 10 / 2, -deviceHeight / 10 / 2, 0.1);
      group.add(rect1)

      let drawed = new Set()
      let duplicatedDrawed = {}
      objects.forEach(object => {
        // "boundsInfo":{"bottom":2340,"left":0,"right":1080,"top":0}
        interRender(object, drawed, duplicatedDrawed)
      })
    }



    // const rect2 = createRectangle(108, 234, 1, 0x00ff00)
    // rect2.position.set(12.6/2, -80.8/2, 12)
    // group.add(rect2)
    function interRender (objectInfo, drawed, duplicatedDrawed) {
      if (objectInfo instanceof Array) {
        objectInfo.forEach(obj => interRender(obj, drawed, duplicatedDrawed))
      } else {
        let object = objectInfo.root
        let boundsInfo = object.boundsInfo
        if (!boundsInfo) {
          console.log('未找到boundsInfo 跳过处理：', objectInfo)
          return
        }
        let width = (boundsInfo.right - boundsInfo.left) * 0.1, height = (boundsInfo.bottom - boundsInfo.top) * 0.1
        let rect = null
        let renderColor = object.renderColor || Math.floor(Math.random() * 0xFFFFFF)
        object.renderColor = renderColor
        // 记录控件是否被重复添加
        let { left, top, right, bottom } = object.boundsInfo
        let key = ((l, t, r, b, d) => `${l}_${t}_${r}_${b}_${d}`)(left, top, right, bottom, object.depth)
        let redrawContentRect = null
        if (drawed.has(key)) {
          console.error('控件重复绘制：', objectInfo)
          duplicatedDrawed[key] = [objectInfo].concat(duplicatedDrawed[key])
          let targetHasContent = duplicatedDrawed[key].filter(obj => !!obj.content)
          if (targetHasContent && targetHasContent.length > 0) {
            redrawContentRect = targetHasContent[0]
          }
        } else {
          duplicatedDrawed[key] = [objectInfo]
        }
        drawed.add(key)
        // 存在带内容的控件 优先显示它
        if (redrawContentRect) {
          boundsInfo = redrawContentRect.root.boundsInfo
          width = (boundsInfo.right - boundsInfo.left) * 0.1
          height = (boundsInfo.bottom - boundsInfo.top) * 0.1
          rect = createRectangle(width, height, 1, renderColor, 0.7, object.content)
        } else {
          if (!object.content) {
            if (object.clickable + '' == 'true') {
              rect = createRectangle(width, height, 1, renderColor, 0.3)
            } else {
              rect = createRectOutline(width, height, 1, renderColor, 0.8)
            }
          } else {
            rect = createRectangle(width, height, 1, renderColor, 0.7, object.content)
          }
        }

        // console.log('put rect', (boundsInfo.right - boundsInfo.left) * 0.1, (boundsInfo.bottom - boundsInfo.top) * 0.1, ' at ', boundsInfo.left * 0.1, boundsInfo.top * 0.1, 2 * object.depth)
        rect.position.set(boundsInfo.left * 0.1 + width / 2, -(boundsInfo.top * 0.1 + height / 2), 1 * object.depth)
        rect.objectInfo = objectInfo
        group.add(rect)
      }
    }

    scene.add(group);

    // Add Raycaster for mouse interaction
    const raycaster = new THREE.Raycaster();
    const mouse = new THREE.Vector2();

    let clickDownStart = 0

    document.getElementById('container').addEventListener("mousedown", (event) => {
      clickDownStart = new Date()
    })

    document.getElementById('container').addEventListener("mouseup", (event) => {
      if (new Date() - clickDownStart > 150) {
        console.log('点击间隔太久', (new Date() - clickDownStart))
        return
      }

      // console.log('click point', event.clientX, event.clientY)
      const target = document.getElementById("container");
      // 获取相对于页面的位置
      const rect = target.getBoundingClientRect();

      // console.log('render point', (event.clientX - rect.left), (event.clientY - rect.top))
      mouse.x = ((event.clientX - rect.left) / render_width) * 2 - 1;
      mouse.y = -((event.clientY - rect.top) / render_height) * 2 + 1;

      raycaster.setFromCamera(mouse, camera);
      const intersects = raycaster.intersectObjects(group.children);

      if (intersects.length > 0) {
        const selectedObject = intersects[0].object;
        // console.log(selectedObject.objectInfo)
        if (selectedObject.objectInfo) {
          vueInstance.showDetail(selectedObject.objectInfo)
        }
        // selectedObject.material.color.set(0x0000ff); // Change color to blue on click
      }
    })
    // document.getElementById('container').addEventListener("click", (event) => {

    // });

    // Render loop
    function animate () {
      requestAnimationFrame(animate);
      controls.update();
      renderer.render(scene, camera);
    }
    animate();

    // 触发事件，暴露模块方法
    window.dispatchEvent(new CustomEvent('module-ready', {
      detail: { rerender }
    }));
  </script>
  <script>
    let rerenderFunc = null
    // 非模块代码
    window.addEventListener('module-ready', (event) => {
      const { rerender } = event.detail;
      rerenderFunc = function (allItems, selectedItems, imageB64) {
        allItems = allItems || []
        selectedItems = selectedItems || []
        let rebuildSelectedObjects = regenerateObjects(allItems, selectedItems)
        // console.log('绘制对象列表：', rebuildSelectedObjects)
        rerender(rebuildSelectedObjects, imageB64)
      }

      /**
       * 根据所有节点列表和选中数组构建用于渲染的矩形列表
       */
      function regenerateObjects (allItems, selectedItems) {
        let tmpSelectCache = new Set()
        selectedItems.forEach(key => tmpSelectCache.add(key))
        return buildWithChildren(allItems, tmpSelectCache)

        function buildWithChildren (items, selectedCahce) {
          return items.map(item => {
            let objectList = []
            if (selectedCahce.has(item.id)) {
              objectList.push(item)
            }
            if (item.children && item.children.length > 0) {
              objectList.push(buildWithChildren(item.children, selectedCahce))
            }
            return objectList
          })
        }
      }
    });

    const vueInstance = new Vue({
      el: '#app',
      vuetify: new Vuetify(),
      data () {
        return {
          drawImageCanvas: true,
          draw3DInfo: false,
          showObjectJson: false,
          showExecuteCode: false,
          executeCode: '',
          executePath: '',
          // wsServerUrl: 'ws://192.168.254.238:8212',
          wsServerUrl: 'ws://192.168.2.104:8212',
          connectStatus: '未连接',
          requesting: false,
          connecting: false,
          uiobjects: typeof objects != 'undefined' && !!objects ? JSON.stringify(objects) : '',
          advance: false,
          // uiobjects: '',
          opened: [],
          treeActive: [],
          visibleOnly: true,
          clickableOnly: false,
          hasContent: false,
          inScreen: false,
          dialog: false,
          itemDetail: '',
          stackItems: [],
          showDetailIdx: 0,
          treeSelectedItems: [],
          // selectedItems: [],
          selectedCache: new Set(),
          deviceHeight: null,
          deviceWidth: null,
          filterFuncStr: '',
          filterFunc: null,
          selectorFuncStr: '',
          selectorFunc: null,
          selectionIndependent: true,
          drawedObjects: [],
          drawDuplicatedObjects: [],
          positionDuplicatedObjects: {},
          winPos: { x: 0, y: 0 },
          devicePos: { x: 0, y: 0 },
          findTargetObj: false,
          posColor: '',
          posBkColor: '',
          clearId: null,
          itemId: null,
          lastFindId: null,
          items: [],
          buildingTimeoutId: null,
          totalWidgets: 0,
          imgBase64: typeof imageBase64 != 'undefined' ? imageBase64 || '' : '',
          drawImageCanvas: true,
          hisDialog: false,
          newDataKey: null,
          hisObjects: [],
          hisLoading: false,
          maxHisData: 25,
          canvasHeight: null,
        }
      },
      filters: {
        stamp2Str: function (timestamp) {
          return formatDate(new Date(timestamp))
        }
      },
      computed: {
        connected: function () {
          return this.connectStatus == 'ready'
        },
        filter: function () {
          console.log('filter changed')
          return (rootNode, childArray) => {
            if (this.visibleOnly) {
              if (!rootNode.visible && !hasVisiableChild(childArray)) {
                return false
              }
            }
            if (this.clickableOnly) {
              if (!(rootNode.clickable + '' == 'true') && !hasClickableChild(childArray)) {
                return false
              }
            }
            if (this.hasContent) {
              if (!rootNode.content && !rootNode.id && !hasValidChild(childArray, this.filter)) {
                return false
              }
            }
            if (this.inScreen) {
              let { left, top, right, bottom } = rootNode.boundsInfo
              console.log('info:', JSON.stringify({ left, top, right, bottom }))
              let inScreen = widthInScreen(left) && heightInScreen(top) && widthInScreen(right) && heightInScreen(bottom)
              if (!inScreen && !hasValidChild(childArray, this.filter)) {
                return false
              }
            }
            if (this.filterFunc) {
              if (!this.filterFunc(rootNode) && !hasValidChild(childArray, this.filter)) {
                return false
              }
            }
            // return rootNode.className=='com.lynx.tasm.ui.image.FlattenUIImage' || childArray.length > 1
            return true
          }
        },
        uiObjectItems: function () {
          let detail = this.itemDetail ? JSON.parse(this.itemDetail) : null
          if (this.stackItems.length > 1) {
            detail = this.stackItems[this.showDetailIdx].root
          }
          if (detail) {
            let itemInfo = detail
            let labelItems = []
            return columns.map(item => ({ label: item.label || item.key, value: item.convert ? item.convert(itemInfo) : itemInfo[item.key] }))
          }
          return []
        },
        selectedItems: function () {
          return this.treeSelectedItems
        },
        selectionType: function () {
          return this.selectionIndependent ? 'independent' : 'leaf'
        },
        treeHeight: function () {
          return this.canvasHeight ? this.canvasHeight + 'px' : '1200px'
        }
      },
      watch: {
        selectedItems: {
          deep: true,
          immediate: true,
          handler: function (newVal, oldVal) {
            console.log(newVal)
            console.log('selectedItems changed:', this.selectedItems.length)//, this.selectedItems)
            if (this.items && this.items.length > 0 && this.selectedItems.length > 0) {
              this.drawCanvas(this.items, this.selectedItems)
            } else {
              console.log('无可绘制节点', this.items)
              this.drawedObjects = []
              this.clearCanvas()
            }
            // 无论有无 都重绘一遍 清空无效节点
            rerenderFunc && rerenderFunc(this.items, this.selectedItems, this.imgBase64)
          }
        },
        uiobjects: {
          handler () {
            this.buildItems()
          }
        },
        filter: {
          handler () {
            this.buildItems()
          },
          immediate: true,
          deep: true
        },
        visibleOnly: function () {
          this.buildItems()
        },
        clickableOnly: function () {
          this.buildItems()
        },
        hasContent: function () {
          this.buildItems()
        },
        inScreen: function () {
          this.buildItems()
        },
        imgBase64: function () {
          this.$el.drawImg.src = this.imgBase64
        },
        draw3DInfo: function (newVal, oldVal) {
          if (newVal) {
            // 隐藏悬浮菜单
            this.findTargetObj = false
          }
        },
        opened: {
          handler () {
            console.log('opened', this.opened)
          },
          immediate: true,
          deep: true
        },
        selectionIndependent: function () {
          console.log(this.$refs.treeNode.open)
        }
      },
      methods: {
        openTarget: function (targetId) {
          if (!targetId) {
            targetId = this.itemId
          }
          this.treeActive = [targetId]
          console.log('所有节点信息', this.items)
          let selectedTarget = new Set()
          selectedTarget.add(targetId)
          this.opened = findOpenedParent(this.items[0], selectedTarget)
        },
        loadHisInfo: function () {
          this.hisDialog = true
          this.hisLoading = true
          DBHelper.getAllData().then(list => {
            this.hisObjects = list.reverse()
            this.hisLoading = false
          })
        },
        deleteData: function (key) {
          DBHelper.deleteData(key).then(() => {
            DBHelper.getAllData().then(list => {
              this.hisObjects = list.reverse()
              this.hisLoading = false
            })
          })
        },
        renderHisObject: function (hisObject) {
          this.uiobjects = hisObject.uiobjects
          this.imgBase64 = hisObject.imgBase64
          this.deviceHeight = hisObject.deviceHeight
          this.deviceWidth = hisObject.deviceWidth
          let _this = this
          setTimeout(() => {
            _this.selectAll()
          }, 500)
          this.hisDialog = false
          this.clearCanvas()
          // treeSelectedItems = []
        },
        connectServer: function () {
          if (!this.wsServerUrl) {
            this.connectStatus = '请输入服务端地址和端口'
            return
          }
          this.connecting = true
          createWebsocket(this.wsServerUrl, this)
        },
        requestWidgetInfo: function () {
          if (!this.connected || this.requesting) {
            return
          }
          this.requesting = true
          let _this = this

          wsApi.request('widget_info', {}).then(resp => {
            _this.requesting = false
            console.log('widget info from server:', JSON.stringify(resp, (key, value) => {
              // 过滤长字段 不在日志中打印
              if (key == 'widgetInfo' || key == 'imgBase64') {
                return undefined
              }
              return value
            }))
            console.log('截图耗时：', resp.captureCost, '控件耗时：', resp.widgetCost, '图像转换耗时：', resp.convertCost)
            if (resp.code == 'success') {
              _this.deviceHeight = resp.height
              _this.deviceWidth = resp.width
              _this.uiobjects = JSON.stringify(JSON.parse(resp.widgetInfo), null, 2)
              _this.imgBase64 = 'data:image/png;base64,' + resp.imgBase64
              _this.newDataKey = new Date().getTime()
              DBHelper.addDataWithLimit({ timestamp: _this.newDataKey, uiobjects: _this.uiobjects, imgBase64: _this.imgBase64 }, _this.maxHisData)
              setTimeout(() => {
                _this.selectAll()
              }, 500)
            } else {
              console.error('请求控件信息异常', resp.msg)
            }
          })
        },
        sendCode: function () {
          if (!this.connected || this.requesting || !this.executeCode) {
            return
          }
          this.requesting = true
          wsApi.request('operate', {
            data: this.executeCode, name: 'tmp', config: {
              path: this.executePath
            }
          }).then(() => this.requesting = false)
        },
        buildItems: function (callback) {
          // debugger
          console.log('触发buildItems')
          // if (this.buildingTimeoutId) {
          //   clearTimeout(this.buildingTimeoutId)
          // }
          let _this = this
          this.buildingTimeoutId = setTimeout(function () {
            if (_this.uiobjects) {
              allKey = []
              allObject = []
              let data = JSON.parse(_this.uiobjects)
              if (data && data.length > 0) {
                deviceHeight = _this.deviceHeight
                deviceWidth = _this.deviceWidth
                packageName = data[0].packageName
                // console.log(data)
                // setTimeout(() => {
                idChecker = new Set()
                // 根据boundsInfo构建key 存储位置重复的对象
                _this.positionDuplicatedObjects = {}
                let root = buildWithChild(data, _this.filter, _this.positionDuplicatedObjects)
                if (_this.selectorFunc) {
                  // setTimeout(function () {
                  // 调用选择函数 自动选中需要选择的控件
                  _this.selectedCache.clear()
                  _this.autoSelect(root, _this.selectorFunc)
                  let list = []
                  _this.selectedCache.forEach(id => {
                    list.push(id)
                  })
                  // debugger
                  // 延迟变更
                  setTimeout(function () {
                    console.log('build items select items:', list.length)
                    _this.treeSelectedItems = list
                    _this.opened = findOpenedParent(root, _this.selectedCache)
                    callback && callback()
                  }, 20)
                  // }, 100)
                } else {
                  callback && callback()
                }
                _this.items = [root]
                _this.totalWidgets = allKey.length
                if (_this.newDataKey) {
                  let key = _this.newDataKey
                  console.log('更新控件信息', _this.newDataKey)
                  _this.newDataKey = null
                  setTimeout(() => {
                    let scale = 50 / deviceHeight, imgHeight = deviceHeight * scale, imgWidth = deviceWidth * scale
                    compressBase64Image(_this.imgBase64, imgWidth, imgHeight, 1).then(smallImg => {
                      DBHelper.updateObjectInfo(key, _this.totalWidgets, smallImg, deviceWidth, deviceHeight)
                    })
                  }, 100)
                }
                // }, 10)
              }
            } else {
              _this.items = []
            }
          }, 10)

        },
        autoSelect: function (obj, selector) {
          if (!obj) {
            return false
          }
          if (selector(obj.root) && !this.selectedCache.has(obj.id)) {
            // console.log('找到匹配的对象 id:', obj.id, 'root:', JSON.stringify(obj.root))
            this.selectedCache.add(obj.id)
          }
          if (obj.children && obj.children.length > 0) {
            obj.children.forEach(child => this.autoSelect(child, selector))
          }
        },
        handleFuncStrChange: function () {
          try {
            console.log('func changed', this.filterFuncStr)
            if (!this.filterFuncStr) {
              this.filterFunc = v => true
              this.buildItems(this.openAll)
              return
            }

            let lambdaCheck = /^\w+\s*=>/
            if (lambdaCheck.test(this.filterFuncStr)) {
              let func = eval(this.filterFuncStr)
              this.filterFunc = func
            } else {
              this.filterFunc = eval('v=>new Selector(v).' + this.filterFuncStr + '.find()')
              console.log('非lambda函数，自动创建selector', this.filterFunc)
            }
            this.buildItems()
          } catch (e) {
            this.filterFunc = null
            console.error(e)
          }
        },
        handleSelectorFuncStrChange: function () {
          try {
            console.log('func changed', this.selectorFuncStr)
            if (!this.selectorFuncStr) {
              this.selectorFunc = null
              this.buildItems(this.openAll)
              return
            }
            let lambdaCheck = /^\w+\s*=>/
            if (lambdaCheck.test(this.selectorFuncStr)) {
              let func = eval(this.selectorFuncStr)
              this.selectorFunc = func
            } else {
              this.selectorFunc = eval('v=>new Selector(v).' + this.selectorFuncStr + '.find()')
              console.log('非lambda函数，自动创建selector', this.selectorFunc)
            }
            this.buildItems()
          } catch (e) {
            this.selectorFunc = null
            console.error(e)
          }
        },
        openAll: function () {
          this.opened = allKey
          // console.log('全部打开', this.$refs.treeNode.open, this.opened)
          // this.$refs.treeNode.updateAll()
        },
        closeAll: function () {
          this.opened = []
        },
        showDetail: function (objectInfo) {
          this.dialog = true
          if (objectInfo) {
            console.log('选中目标：', objectInfo)
            this.itemDetail = JSON.stringify(objectInfo.root, true)
            // 用于删除对象
            this.itemId = objectInfo.id
          }
          this.stackItems = this.getStackObjects()
          this.showDetailIdx = 0
        },
        selectAll: function () {
          this.treeSelectedItems = allKey
        },
        handleActive: function (items) {
          console.log({
            key: 'active'
          }, items)
        },
        handleOpen: function (opened) {
          this.opened = opened
        },
        clearCanvas: async function () {
          if (this.$el && this.$el.canvas) {
            this.$el.canvas.width = this.$el.canvas.width
            this.$el.canvas.height = canvasWidth / deviceWidth * deviceHeight
            let scale = canvasWidth / deviceWidth
            let _this = this
            if (_this.$el.drawImg && _this.$el.drawImg.src && _this.drawImageCanvas) {
              _this.$el.ctx.drawImage(_this.$el.drawImg, 0, 0, _this.$el.canvas.width, _this.$el.canvas.height)
            }
            this.$el.ctx.scale(scale, scale)
          }
        },
        drawCanvas: async function (uiObjects, selectedItems) {
          if (!(uiObjects && uiObjects.length > 0)) {
            return
          }
          let tmpSelectCache = new Set()
          this.drawedObjects = []
          this.drawDuplicatedObjects = []
          selectedItems.forEach(key => tmpSelectCache.add(key))
          let drawed = new Set()
          console.log('清空canvas')
          await this.clearCanvas()
          this.$el.ctx.strokeStyle = "#23ff00";
          console.log('绘制')
          let start = new Date().getTime()
          // 记录是否存在重复绘制的对象
          window.drawedObjectIds = new Set()
          await uiObjects.forEach(object => this.drawChild(object, tmpSelectCache, this.$el.ctx, drawed))
          console.log('绘制完毕，cost', new Date().getTime() - start, 'ms')
        },
        drawChild: function (object, selectedCache, ctx, drawed) {
          let { left, top, right, bottom } = object.root.boundsInfo
          let key = `${left}_${top}_${right}_${bottom}`
          if (selectedCache.has(object.id) && !drawed.has(key) && boundsInScreen(left, top, right, bottom)) {
            // console.log('绘制：', left, top, right - left, bottom - top)
            ctx.strokeRect(left, top, right - left, bottom - top)
            drawed.add(key)
            window.drawedObjectIds.add(object.id)
            this.drawedObjects.push(object)
          } else if (drawed.has(key)) {
            this.drawDuplicatedObjects.push(object)
          }
          if (object.children && object.children.length > 0) {
            object.children.forEach(child => this.drawChild(child, selectedCache, ctx, drawed))
          }
        },
        findTarget: async function (x, y, showMenu) {
          let realX = x * deviceWidth / canvasWidth
          let realY = y * deviceWidth / canvasWidth
          // console.log('实际坐标：', `${realX}, ${realY}`)
          let findObjects = this.drawedObjects.filter(({ root: r }) => {
            let { left, top, right, bottom } = r.boundsInfo
            r.distance = realX - left + right - realX + bottom - realY + realY - top
            return left <= realX && right >= realX && top <= realY && bottom >= realY
          }).sort((a, b) => a.root.distance - b.root.distance)
          if (findObjects.length > 0) {
            this.clearCanvas()
            this.$el.ctx.strokeStyle = '#23ff00'
            this.drawedObjects.forEach(obj => {
              let { left, top, right, bottom } = obj.root.boundsInfo
              this.$el.ctx.strokeRect(left, top, right - left, bottom - top)
            })
            // console.log('找到了匹配的对象:', JSON.stringify(findObjects))
            let _this = this
            this.itemDetail = JSON.stringify(findObjects[0].root)
            this.itemId = findObjects[0].id
            if (showMenu) {
              this.lastFindId = this.itemId
            }
            this.$el.ctx.strokeStyle = '#ff0000'
            let { left, top, right, bottom } = findObjects[0].root.boundsInfo
            // console.log('绘制rect', left, top, right - left, bottom - top)
            this.$el.ctx.strokeRect(left, top, right - left, bottom - top)
          }
          this.findTargetObj = this.itemId && this.itemId == this.lastFindId
          if (!this.findTargetObj) {
            this.lastFindId = null
          }
        },
        clickCenter: function () {
          let detail = this.itemDetail ? JSON.parse(this.itemDetail) : null
          if (this.stackItems && this.stackItems.length > 1) {
            detail = this.stackItems[this.showDetailIdx].root
          }
          if (ws && isConnected && detail) {
            let { left, top, right, bottom } = detail.boundsInfo
            wsApi.request('operate', {
              data: ((x, y) => {
                return `(() => {
                  //toastLog('点击位置：${x} ${y}')
                  automator.click(${x}, ${y})
              })()`
              })(left + (right - left) / 2, top + (bottom - top) / 2).toString()
            })
              .then(() => setTimeout(() => {
                this.requestWidgetInfo()
              }, 500))
            this.itemId == null
            this.lastFindId = null
            this.findTargetObj = false
          }
        },
        clickPos: function () {
          if (ws && isConnected) {
            wsApi.request('operate', {
              data: ((x, y) => {
                return `(() => {
                  //toastLog('点击位置：${x} ${y}')
                  automator.click(${x}, ${y})
              })()`
              })(this.devicePos.x, this.devicePos.y).toString()
            })
              .then(() => setTimeout(() => {
                this.requestWidgetInfo()
              }, 500))

            this.itemId == null
            this.lastFindId = null
            this.findTargetObj = false
          }
        },
        removeItem: function () {
          let idx = this.selectedItems.indexOf(this.itemId)
          this.selectedItems.splice(idx, 1)
          let stackObjects = this.getStackObjects()
          console.log('堆叠数：', stackObjects.length)
          if (stackObjects.length > 1) {
            stackObjects.forEach(remove => {
              let idx = this.selectedItems.indexOf(remove.id)
              if (idx > -1) {
                console.log('remove', remove.id, 'idx', idx)

                this.selectedItems.splice(idx, 1)
              }
            })
          }
          this.itemId == null
          this.lastFindId = null
          this.findTargetObj = false
          this.dialog = false
        },
        getStackObjects: function () {
          let detailObj = JSON.parse(this.itemDetail)
          let key = buildKey(detailObj)
          return this.positionDuplicatedObjects[key] || []

          function buildKey (root) {
            let { left, top, right, bottom } = root.boundsInfo
            return `${left}_${top}_${right}_${bottom}`
          }
        }
      },
      mounted () {
        DBHelper.waitForConnected().then(helper => {
          helper.getAllData().then(resultList => {
            console.log('历史数据：', resultList)
          })
        })
        this.$el.canvas = document.getElementById('myCanvas')
        this.$el.ctx = this.$el.canvas.getContext('2d')
        let _this = this
        this.$el.canvas.onclick = function (event) {
          let x = event.offsetX;
          let y = event.offsetY;
          console.log('点击位置：', `${x}, ${y}`, event)
          _this.winPos = {
            x: event.pageX,
            y: event.pageY,
          }
          let scale = _this.$el.canvas.width / _this.deviceWidth
          _this.devicePos = {
            x: x / scale,
            y: y / scale,
          }
          _this.findTarget(x, y, true)
        }

        this.$el.canvas.onmousemove = function (e) {
          let x = event.offsetX;
          let y = event.offsetY;
          _this.clearId && clearTimeout(_this.clearId)
          _this.clearId = setTimeout(() => {
            _this.findTarget(x, y)
          }, 5)
          // 显示悬浮窗时不再动态提取
          if (_this.findTargetObj) {
            return
          }
          let scale = deviceWidth / canvasWidth
          let realX = Math.ceil(x * scale)
          let realY = Math.ceil(y * scale)
          _this.devicePos = { x: realX, y: realY }
          function getColorHexAt (x, y) {
            const imageData = _this.$el.ctx.getImageData(x, y, 1, 1); // 获取 (x, y) 像素颜色
            const [r, g, b] = imageData.data; // 提取 RGB 值
            const toHex = (value) => value.toString(16).padStart(2, "0"); // 转换为两位十六进制
            return [`#${toHex(r)}${toHex(g)}${toHex(b)}`.toUpperCase(),
            `#${toHex(255 - r)}${toHex(255 - g)}${toHex(255 - b)}`.toUpperCase(),
            ]; // 返回 HEX 格式
          }
          let colorstrs = getColorHexAt(x, y)
          _this.posColor = colorstrs[0]
          _this.posBkColor = colorstrs[1]
        }
        setTimeout(() => {
          _this.buildItems(_this.openAll)
        }, 10)
        function recalculate () {
          canvasWidth = Math.min(600, window.innerWidth - 20)
          console.log('计算canvas width:', canvasWidth)
          _this.$el.canvas.width = canvasWidth
          _this.$el.canvas.height = canvasWidth / deviceWidth * deviceHeight
          _this.canvasHeight = _this.$el.canvas.height
          if (_this.$el.drawImg && _this.$el.drawImg.src && _this.drawImageCanvas) {
            _this.$el.ctx.drawImage(_this.$el.drawImg, 0, 0, _this.$el.canvas.width, _this.$el.canvas.height)
          }
        }
        recalculate()
        let calculating = null
        window.onresize = function () {
          if (calculating) {
            clearTimeout(calculating)
          }
          calculating = setTimeout(() => recalculate(), 50)
        }
        this.$el.drawImg = new Image()
        window.onload = function () {
          console.log('绑定img load')
          _this.$el.drawImg.onload = function () {
            console.log('image load:', _this.$el.drawImg.width, ',', _this.$el.drawImg.height)
            if (_this.drawImageCanvas) {
              recalculate()
            }
          }
          setTimeout(() => {
            console.log('对img赋值', _this.imgBase64)
            _this.$el.drawImg.src = _this.imgBase64
          }, 100)
        }
      }
    })
  </script>

</body>

</html>